﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CkSoftware.GroupMe.Sdk.Core;
using CkSoftware.GroupMe.Sdk.Core.Dao;
using CkSoftware.GroupMe.Sdk.Core.Exceptions;
using CkSoftware.GroupMe.Sdk.Groups.Commands;
using CkSoftware.GroupMe.Sdk.Groups.Dao;
using CkSoftware.GroupMe.Sdk.Messaging;

namespace CkSoftware.GroupMe.Sdk.Groups
{
	/// <summary>
	/// The factory class for GroupMe groups.
	/// </summary>
	public class GroupMeGroupFactory
	{
		private readonly IWebApiAccess _webApiAccess;

		/// <summary>
		/// Instantiates a new <see cref="GroupMeGroupFactory" /> object.
		/// </summary>
		/// <param name="webApiAccess">The <see cref="IWebApiAccess" /> used to call the API.</param>
		public GroupMeGroupFactory(IWebApiAccess webApiAccess)
		{
			_webApiAccess = webApiAccess;
		}

		/// <summary>
		/// Creates a new group with the given options.
		/// </summary>
		/// <param name="groupName">The name of the new group.</param>
		/// <param name="description">The description of the new group.</param>
		/// <param name="groupImage">The group-avatar image.</param>
		/// <param name="openForSharing">Defines if this group should be open for sharing / free to join.</param>
		/// <exception cref="ArgumentNullException">Occurs if the group name is null or an empty string.</exception>
		/// <returns>An awaitable Task resulting in a <see cref="GroupMeGroup" /> instance.</returns>
		public async Task<GroupMeGroup> CreateGroup(string groupName, string description = "", PictureData groupImage = null,
			bool openForSharing = false)
		{
			if (string.IsNullOrEmpty(groupName))
			{
				throw new ArgumentNullException("groupName");
			}

			var command = new CreateGroupCommand(groupName);

			if (!string.IsNullOrEmpty(description))
			{
				command.WithDescription(description);
			}

			if (groupImage != null)
			{
				command.WithImage(groupImage);
			}

			if (openForSharing)
			{
				command.OpenForSharing();
			}

			GroupMeApiResponse<GroupData> groupData = await _webApiAccess.ExecuteCommand(command);

			if (groupData.Meta.ParsedCode != GroupMeApiResultCode.Created)
			{
				throw new GroupMeException(groupData.Meta);
			}

			InitializeGroupMemberDirectMessages(_webApiAccess, groupData.Response);
			return new GroupMeGroup(_webApiAccess, groupData.Response);
		}

		/// <summary>
		/// Destroys the group with the given groupId.
		/// </summary>
		/// <param name="groupId">The id of the group which should be destroyed.</param>
		/// <exception cref="ArgumentNullException">Occurs if the groupId parameter is null or an empty string.</exception>
		/// <returns>An awaitable task.</returns>
		public async Task DestroyGroup(string groupId)
		{
			if (string.IsNullOrEmpty(groupId))
			{
				throw new ArgumentNullException("groupId");
			}

			var command = new DestroyGroupCommand(groupId);
			await _webApiAccess.ExecuteCommand(command);
		}

		/// <summary>
		/// Gets a list of former (and re-joinable) groups for the currently authenticated user.
		/// </summary>
		/// <returns>An awaitable Task resulting in a list of <see cref="GroupMeGroup"/>s.</returns>
		public async Task<IEnumerable<GroupMeGroup>> GetFormerGroups()
		{
			var command = new FormerGroupsCommand();
			GroupMeApiResponse<GroupData[]> formerGroupsData = await _webApiAccess.ExecuteCommand(command);

			if (formerGroupsData.Meta.ParsedCode != GroupMeApiResultCode.Ok)
			{
				throw new GroupMeException(formerGroupsData.Meta);
			}

			InitializeGroupMemberDirectMessages(_webApiAccess, formerGroupsData.Response);
			return formerGroupsData.Response.Select(data => new GroupMeGroup(_webApiAccess, data));
		}

		/// <summary>
		/// Gets information about the group with the given group id.
		/// </summary>
		/// <param name="groupId">The group id to query the group data.</param>
		/// <returns>An awaitable Task resulting in a <see cref="GroupMeGroup"/>.</returns>
		public async Task<GroupMeGroup> GetGroup(string groupId)
		{
			var command = new ShowGroupCommand(groupId);
			GroupMeApiResponse<GroupData> groupData = await _webApiAccess.ExecuteCommand(command);

			if (groupData.Meta.ParsedCode != GroupMeApiResultCode.Ok)
			{
				throw new GroupMeException(groupData.Meta);
			}

			InitializeGroupMemberDirectMessages(_webApiAccess, groupData.Response);
			return new GroupMeGroup(_webApiAccess, groupData.Response);
		}

		/// <summary>
		/// Gets all currently active groups for the currently authenticated user.
		/// </summary>
		/// <returns>An awaitable Task resulting in a <see cref="GroupsListing"/>.</returns>
		public async Task<GroupsListing> GetGroups()
		{
			var command = new GroupIndexCommand();
			GroupMeApiResponse<GroupData[]> groupData = await _webApiAccess.ExecuteCommand(command);

			if (groupData.Meta.ParsedCode == GroupMeApiResultCode.Ok)
			{
				InitializeGroupMemberDirectMessages(_webApiAccess, groupData.Response);
				return new GroupsListing(_webApiAccess, command, groupData.Response);
			}

			throw new GroupMeException(groupData.Meta);
		}

		/// <summary>
		/// Updates the group with the given groupId with the given data. Attention: only new data must be added, so if you only want to change the description, only add the description parameter.
		/// </summary>
		/// <param name="groupId">The group id of the group to update.</param>
		/// <param name="newGroupName">The new group name.</param>
		/// <param name="newDescription">The new group description.</param>
		/// <param name="newGroupImage">The new group image.</param>
		/// <param name="openForSharing">The new value for the openForSharing property.</param>
		/// <exception cref="ArgumentNullException">Occurs if the groupId is null or an empty string.</exception>
		/// <returns>An awaitable Task resulting in an updated <see cref="GroupMeGroup"/>.</returns>
		public async Task<GroupMeGroup> UpdateGroup(string groupId,
			string newGroupName = "",
			string newDescription = "",
			PictureData newGroupImage = null,
			bool openForSharing = false)
		{
			if (string.IsNullOrEmpty(groupId))
			{
				throw new ArgumentNullException("groupId");
			}

			var command = new UpdateGroupCommand(groupId);

			if (!string.IsNullOrEmpty(newGroupName))
			{
				command.WithName(newGroupName);
			}

			if (!string.IsNullOrEmpty(newDescription))
			{
				command.WithDescription(newDescription);
			}

			if (newGroupImage != null)
			{
				command.WithImage(newGroupImage);
			}

			if (openForSharing)
			{
				command.OpenForSharing();
			}

			GroupMeApiResponse<GroupData> groupData = await _webApiAccess.ExecuteCommand(command);

			if (groupData.Meta.ParsedCode != GroupMeApiResultCode.Ok)
			{
				throw new GroupMeException(groupData.Meta);
			}

			InitializeGroupMemberDirectMessages(_webApiAccess, groupData.Response);

			return new GroupMeGroup(_webApiAccess, groupData.Response);
		}

		private static void InitializeGroupMemberDirectMessages(IWebApiAccess webApiAccess, IEnumerable<GroupData> groups)
		{
			foreach (GroupData group in groups)
			{
				InitializeGroupMemberDirectMessages(webApiAccess, group);
			}
		}

		private static void InitializeGroupMemberDirectMessages(IWebApiAccess webApiAccess, GroupData group)
		{
			foreach (GroupMemberData memberData in group.Members)
			{
				GroupMemberData memberDataForEnclosure = memberData;
				memberData.Messages =
					new Lazy<GroupMeDirectMessages>(() => new GroupMeDirectMessages(webApiAccess, memberDataForEnclosure.UserId));
			}
		}
	}
}